package
{
    import flash.display.BitmapData;
    import flash.display.Shape;
    import flash.events.EventDispatcher;
    import flash.geom.Matrix;
    import flash.geom.Point;
    import flash.geom.Rectangle;
    import flash.utils.Dictionary;

    /**
     *
     * @author jjung
     */
    public class Microbe extends EventDispatcher
    {
        //---------------------------------------------------------------------
        //
        //  Static Variables
        //
        //---------------------------------------------------------------------
        /** Basically since we suck out a value from 0.0 - 1.0, we need to know when to
         * subtract from the environment. Because the graphics hold values from 0 - 255 INTEGER,
         * we have to wait until a threshold is reached before we can actually draw
         * a circle subtracting values from the environment. Otherwise we might never
         * actually subtract anything because the threshold is smaller than 1 INTEGER.
         */
        public static var absorptionRenderThreshold : Number = 0.05;

        public static var START_MASS : Number = 0.3; // 128 ( 0 -256)
        public static var START_CELLWALL : Number = 0x80; // 128 ( 0 -256)
        public static var START_ACCEL : Number = 0x80; // 128 ( 0 -256)

        public static var MAX_DIAMETER : Number = 17;

        public static function get MAX_RADIUS() : Number
        {
            return MAX_DIAMETER / 2;
        }

        public static var mat : Matrix;

        /**
         * All of the microbes in the application - by their ID.
         */
        public static var microbesById : Dictionary = new Dictionary(false);

        //---------------------------------------------------------------------
        //
        //  Variables
        //
        //---------------------------------------------------------------------
        /**
         * The id of this microbe.
         */
        public var id : int;

        /**
         * These are all the properties that can evolve on this microbe.
         *
         * Basically evolution change will be a random number between inc and dec and will never
         * exceed max and never fall below min.
         *
         * Every time an evolution occurs, one property will increase and another will decrease,
         * creating a system of trade-offs.
         *
         * Type: Evolver
         */
        public var evolvers : Array = [];

        /**
         * The amount of mass that is gained every second in a perfect nutrient solution.
         */
        public var growthRate : Number = 0.3; // 0 - 1.0

        /**
         * The amount of toxins taken in in one second in a pure toxin solution.
         */
        public var toxinRate : Number = 0.2; // 0 - 1.0

        /**
         * The amount of nutrients the cell needs to maintain its size and not shrink.
         */
        public var staticNutrientLoss : Number = 0.01;

        /**
         * The amount of toxins lost per second when the cell is doing nothing.
         */
        public var staticToxinLoss : Number = 0.1;

        /**
         * If mass goes below this point the cell dies.
         */
        public var nutrientMassDeath : Number = 0.1;

        /**
         * When the ratio of toxins to nutrients exceeds this threshold, the cell dies.
         */
        public var toxinRatioDeath : Number = 0.8;



        public var bd : BitmapData;
        public var shape : Shape;

        public var x : Number;
        public var y : Number;

        public var visible : Boolean = true;
        public var mitosis : Boolean = false;

        /**
         * The amount of nutrients the microbe has sucked out of its environment.
         * Basically every time this hits a certain amount, we subtract some of the green
         * from the nutrients layer and reset this.
         */
        public var nutrientsAbsorbedCounter : Number = 0;

        /**
         * 0 - 1.0. Resistance to Toxicity: thickness of the cell wall.
         * Amount of toxic entry to cell is determined by difference between toxicity of environment and
         * cellWall.
         */
        public var cellWall : uint = 0.1;

        /**
         * The amount of toxins the microbe has sucked out of its environment.
         * Basically every time this hits a certain amount, we subtract some of the green
         * from the nutrients layer and reset this.
         */
        public var toxinsAbsorbedCounter : Number = 0;

        /**
         * 0 - 1.0. Acceleration of the cell.
         * Acceleration determines how fast the cell can move through its environment
         */
        public var accel : Number = 20;

        public var speedX : Number = 0;
        public var speedY : Number = 0;

        public var selected : Boolean = false;
        public var destination : Point;

        //---------------------------------------------------------------------
        //
        //  Properties
        //
        //---------------------------------------------------------------------
        //-----------------------------
        // toxinMass
        //-----------------------------
        /**
         * @private
         */
        protected var _toxinMass : Number = 0;

        /**
         * @default 0
         */
        public function get toxinMass() : Number
        {
            return _toxinMass;
        }

        /**
         * @private
         */
        public function set toxinMass(value : Number) : void
        {
            if (value == _toxinMass)
            {
                return;
            }

            _toxinMass = value;

            if (_toxinMass < 0.0)
                _toxinMass = 0.0;
            if (_toxinMass > 1.0)
                _toxinMass = 1.0;
        }

        //-----------------------------
        // nutrientMass
        //-----------------------------
        /**
         * @private
         */
        protected var _nutrientMass : Number = 0;

        /**
         * @default 0
         */
        public function get nutrientMass() : Number
        {
            return _nutrientMass;
        }

        /**
         * @private
         */
        public function set nutrientMass(value : Number) : void
        {
            if (value == _nutrientMass)
            {
                return;
            }

            _nutrientMass = value;

            if (_nutrientMass < 0.0)
                _nutrientMass = 0.0;
            if (_nutrientMass > 1.0)
                _nutrientMass = 1.0;
        }

        //-----------------------------
        // mass
        //-----------------------------
        public function get mass() : Number
        {
            return (toxinMass + nutrientMass) / 2.0;
        }

        //-----------------------------
        // toxinsToNutrients
        //-----------------------------
        // The ratio of toxins to nutrients in this cell. 1.0 = all toxins. 0.0 = all nutrients.
        public function get toxinsToNutrients() : Number
        {
            return toxinMass / nutrientMass;
        }

        //---------------------------------------------------------------------
        //
        //  Properties
        //
        //---------------------------------------------------------------------
        public function get radius() : Number
        {
            return Math.sqrt(mass / Math.PI) * (MAX_DIAMETER / 2);
        }

        public function get diameter() : Number
        {
            return radius * 2;
        }

        //---------------------------------------------------------------------
        //
        //  Constructor
        //
        //---------------------------------------------------------------------
        /**
         *  Constructor
         */
        public function Microbe(id : int)
        {
            this.id = id;

            evolvers.push(new NumberEvolver(this, "accel", 5.0, -4.0, 100, 10, 0.5));

            bd = new BitmapData(MAX_DIAMETER, MAX_DIAMETER, true, 0x00000000);
            shape = new Shape();

            nutrientMass = START_MASS;
            toxinMass = 0.0;

            cellWall = START_CELLWALL;

            accel = 1000;

            mat = new Matrix();
            mat.tx = MAX_RADIUS;
            mat.ty = MAX_RADIUS;
        }

        //---------------------------------------------------------------------
        //
        //  Methods
        //
        //---------------------------------------------------------------------
        public function resetRender() : void
        {
            bd = new BitmapData(MAX_DIAMETER, MAX_DIAMETER, true, 0x00000000);
        }

        public function resetPhysics() : void
        {
            speedX = 0;
            speedY = 0;
        }

        public function update(timeElapsed : Number, environment : Environment) : void
        {
            var accelX : Number = 0;
            var accelY : Number = 0;

            if (mitosis)
            {
                return;
            }

            var angle : Number = 0;
            var pixelsToCheck : Array = [];
            var p : Point;
            var iMaxNutrients : int = -1;
            var maxNutrients : Number = 0;

            for (var i : int = 0; i < 8; i++)
            {
                p = new Point();

                p.x = x + Math.cos(angle) * (radius / 2);
                p.y = y + Math.sin(angle) * (radius / 2);

                pixelsToCheck.push(p);

                angle += Math.PI / 4;
            }

            var avgTox : Number = 0;
            var avgNut : Number = 0;
            var avgFlu : Number = 0;

            for (var i : int = 0; i < pixelsToCheck.length; i++)
            {
                var pix : uint = environment.bdToxic.getPixel(pixelsToCheck[i].x, pixelsToCheck[i].y);
                avgTox += (pix >> 16) & 0xFF;

                pix = environment.bdNutrients.getPixel(pixelsToCheck[i].x, pixelsToCheck[i].y);
                var nut : Number = uint((pix >> 8) & 0xFF);
                avgNut += nut;

                pix = environment.bdFluidity.getPixel(pixelsToCheck[i].x, pixelsToCheck[i].y);
                avgFlu += (pix) & 0xFF;

                if (nut > maxNutrients)
                {
                    maxNutrients = nut;
                    iMaxNutrients = i;
                }
            }

            if (!destination)
            {
                // Let's calculate movement based on the direction that leads to the most food.
                if (iMaxNutrients != -1)
                {
                    accelX += Math.cos((Math.PI / 4) * iMaxNutrients) * accel;
                    accelY += Math.sin((Math.PI / 4) * iMaxNutrients) * accel;
                }
            }
            else
            {
                var xDir : Number = destination.x - x;
                var yDir : Number = destination.y - y;

                var len : Number = Math.sqrt(xDir * xDir + yDir * yDir);
                xDir /= len;
                yDir /= len;

                accelX += xDir * accel;
                accelY += yDir * accel;
            }

            speedX += accelX * timeElapsed;
            speedY += accelY * timeElapsed;

            x += speedX * timeElapsed;
            y += speedY * timeElapsed;

            if (destination)
            {
                if (Math.abs(x - destination.x) < 5 && Math.abs(y - destination.y) < 5)
                {
                    destination = null;
                }
            }
            avgTox = (avgTox / pixelsToCheck.length) / 255;
            avgNut = (avgNut / pixelsToCheck.length) / 255;
            avgFlu = (avgFlu / pixelsToCheck.length) / 255;

            // Friction
            speedX -= speedX * avgFlu;
            speedY -= speedY * avgFlu;

            // An increase of 255 mass every 60 seconds
            var growthIntake : Number = growthRate * timeElapsed * avgNut;
            var nutrientLoss : Number = -staticNutrientLoss * timeElapsed;
            nutrientLoss -= (Math.abs(accelX) / accel) * timeElapsed * 0.05;
            nutrientLoss -= (Math.abs(accelY) / accel) * timeElapsed * 0.05;

            var nutrientMassChange : Number = growthIntake + nutrientLoss;

            nutrientsAbsorbedCounter += growthIntake;
            nutrientMass += nutrientMassChange;

            var toxinIntake : Number = toxinRate * timeElapsed * avgTox;
            var toxinLoss : Number = staticToxinLoss * timeElapsed;

            toxinsAbsorbedCounter += toxinIntake * (1.0 - cellWall);
            toxinMass += (toxinIntake * (1.0 - cellWall)) - toxinLoss;

            if (toxinsAbsorbedCounter >= absorptionRenderThreshold)
            {
                MicrobialLife.getInstance().environment.subtractToxins(x, y, radius * 3, toxinsAbsorbedCounter);

                toxinsAbsorbedCounter = 0;
            }

            if (toxinsToNutrients >= toxinRatioDeath)
            {
                MicrobialLife.getInstance().microbesToSplice.push(this);
            }

            if (nutrientsAbsorbedCounter > absorptionRenderThreshold)
            {
                MicrobialLife.getInstance().environment.subtractNutrients(x, y, radius * 3, nutrientsAbsorbedCounter);

                nutrientsAbsorbedCounter = 0;
            }

            if (nutrientMass >= 0.99)
            {
                new Mitosis(this, splitSelf());
            }
            else if (nutrientMass < nutrientMassDeath)
            {
                MicrobialLife.getInstance().microbesToSplice.push(this);
            }

            // Check bounds
            if (x < 0)
                x = 0;
            if (x > 800)
                x = 800;
            if (y < 0)
                y = 0;
            if (y > 600)
                y = 600;
        }

        public function splitSelf() : Microbe
        {
            var newMicrobe : Microbe = new Microbe();
            newMicrobe.x = x;
            newMicrobe.y = y;
            newMicrobe.toxinMass = toxinMass;
            newMicrobe.nutrientMass = nutrientMass;
            newMicrobe.accel = accel;
            newMicrobe.cellWall = cellWall;
            newMicrobe.destination = destination;
            newMicrobe.growthRate = growthRate;
            newMicrobe.nutrientMassDeath = nutrientMassDeath;
            newMicrobe.staticNutrientLoss = staticNutrientLoss;
            newMicrobe.staticToxinLoss = staticToxinLoss;
            newMicrobe.toxinRate = toxinRate;

            return newMicrobe;
        }

        public function get fillColor() : uint
        {
            var fillColor : uint = 0x0;

            // Red = cellWall.
            fillColor |= uint(toxinsToNutrients * 255) << 16;

            // Green = energy.
            fillColor |= uint((1.0 - toxinsToNutrients) * 255) << 8;

            // Blue = accel.
            fillColor |= accel;

            return fillColor;
        }

        var frame : int = 0;

        public function render()
        {
            if (frame == 3)
            {
                shape.graphics.clear();

                shape.graphics.beginFill(0xFFFFFF, 1.0);
                shape.graphics.drawCircle(0, 0, radius);
                shape.graphics.endFill();

                shape.graphics.beginFill(fillColor, 1.0);
                shape.graphics.drawCircle(0, 0, radius - 1);
                shape.graphics.endFill();

                if (selected)
                {
                    shape.graphics.lineStyle(1, 0xFFFFFF, 0.8);
                    shape.graphics.drawRect(-(radius + 2), -(radius + 2), diameter + 4, diameter + 4);
                }



                bd.fillRect(new Rectangle(0, 0, MAX_DIAMETER, MAX_DIAMETER), 0x000000);
                bd.draw(shape, mat, null, null, null, false);
                frame = 0;
            }
            else
            {
                frame++;
            }
        }
    }
}
